"""\
Asteroids is a little game re-write from Panda3d's Sample Program.
(ref: http://www.panda3d.org/wiki/index.php/Sample_Programs:_Asteroids)

It provides GameLoad, GameTerminate, GameRun, GamePause, GameOver, GameReConfigure
for GameMain(GameMain.py).

GameWorld.__init__()
    Create a GameWorld of Asteroids.

GameWorld.GameLoad()
    Process when game-loading.

GameWorld.GameTerminate()
    Process when game-termination.

GameWorld.GameRun()
    Process when game-running.

GameWorld.GamePause()
    Process when game-pausing.

GameWorld.GameOver()
    Process when game-over.

GameWorld.GameReConfigure()
    Process when game-reConfigure.
"""

# event name of game fsm
from Shell.data.GameEventDefine import event_GAME_BACKTOMAINMENU, event_GAME_GAMELOAD, event_GAME_GAMEOVER
from Shell.data.GameEventDefine import event_GAME_GAMEPAUSE, event_GAME_GAMERESUME
# task of main game loop
from GameTaskDefine import task_MAINGAMELOOP

# controller configure ---------------------------------------------
import Shell.controller.EventDefine as ControllerEventDefine
from Shell.controller.ControllerConfigure import ControllerConfigure # be careful for name space collision with panda3d

# Game Load Info / Game Over Info
import Shell.data.GameLoadInfo as GameLoadInfo
import Shell.data.GameOverInfo as GameOverInfo

# import constant --------------------------------------------------
from ConstantDefine import SPRITE_POS, SCREEN_X, SCREEN_Y, TURN_RATE, ACCELERATION
from ConstantDefine import MAX_VEL, MAX_VEL_SQ, DEG_TO_RAD, BULLET_LIFE, BULLET_REPEAT, BULLET_SPEED
from ConstantDefine import AST_INIT_VEL, AST_INIT_SCALE, AST_VEL_SCALE, AST_SIZE_SCALE, AST_MIN_SCALE

#
import direct.directbase.DirectStart
from pandac.PandaModules import TextNode
from pandac.PandaModules import Point2,Point3,Vec3,Vec4
from direct.gui.OnscreenText import OnscreenText
from direct.showbase.DirectObject import DirectObject
from direct.task.Task import Task
from math import sin, cos, pi
from random import randint, choice, random
from direct.interval.MetaInterval import Sequence
from direct.interval.FunctionInterval import Wait,Func
import cPickle


# parent of object --------------------------------------------------
# parent of 3d object
MainNode = None
def MainNodeGen():
  global MainNode
  MainNode = camera.attachNewNode("main node")

def MainNodeDestroy():
  global MainNode
  #MainNode.detachNode()
  MainNode.removeNode()
  #MainNode.remove()

# parent of DirectGUI
from direct.gui.DirectGui import DirectFrame
MainFrame = None
def MainFrameGen():
  global MainFrame
  MainFrame = DirectFrame()
def MainFrameDestroy():
  global MainFrame
  MainFrame.destroy()

# game over event trigger -------------------------------------------
def GameOverEventTrigger():
  messenger.send(event_GAME_GAMEOVER)

# subroutine of create object ---------------------------------------
def loadObject(tex = None, pos = Point2(0,0), depth = SPRITE_POS, scale = 1,
               transparency = True):
  #obj = loader.loadModel("models/plane") #Every object uses the plane model
  obj = loader.loadModel("Shell/ghost/models/plane")
  obj.reparentTo(MainNode)              #Everything is parented to the camera so
                                      #that it faces the screen
  obj.setPos(Point3(pos.getX(), depth, pos.getY())) #Set initial position
  obj.setScale(scale)                 #Set initial scale
  obj.setBin("unsorted", 0)           #This tells Panda not to worry about the
                                      #order this is drawn in. (it prevents an
                                      #effect known as z-fighting)
  obj.setDepthTest(False)             #Tells panda not to check if something
                                      #has already drawn in front of it
                                      #(Everything in this game is at the same
                                      #depth anyway)
  if transparency: obj.setTransparency(1) #All of our objects are trasnparent
  if tex:
    #tex = loader.loadTexture("textures/"+tex+".png") #Load the texture
    tex = loader.loadTexture("Shell/ghost/textures/"+tex+".png") #Load the texture
    obj.setTexture(tex, 1)                           #Set the texture

  return obj

#Macro-like function used to reduce the amount to code needed to create the
#on screen instructions
font = loader.loadFont("cmss12")
def genLabelText(text, i):
  return OnscreenText(parent = MainFrame,
                      text = text, pos = (-1.3, .95-.05*i), fg=(1,1,0,1),
                      align = TextNode.ALeft, scale = .05, font = font)

class GameWorld(DirectObject):
  """\
GameWorld.__init__()
    Create a GameWorld."""
  def __init__(self):
      pass

  # exported control functions -----------------------------------
  def GameLoad(self):
    """\
GameWorld.GameLoad()
    Process when game-loading.
    Create objects."""
    self.GameLoadInfoSetting()
    self.ControllerEventSet()  # event of controller
    self.GUISet()              # 2d gui
    self.ObjectGen()           # 3d object
    self.GameRecordSet()       # game record
    self.MainLoopSet()         # main loop

  def GameTerminate(self):
    """\
GameWorld.GameTerminate()
    Process when game-termination.
    Destroy objects"""
    self.ControllerEventDestroy()  # event of controller
    self.MainLoopDestroy()         # main loop
    self.GUIDestroy()              # 2d gui
    self.ObjectDestroy()           # 3d object
    self.GameRecordDestroy()       # game record

  def GameRun(self):
    """\
GameWorld.GameRun()
    Process when game-running.
    Create game-loop task."""
    self.MainLoopRun()

  def GamePause(self):
    """\
GameWorld.GamePause()
    Process when game-pausing.
    Remove game-loop task."""
    #print taskMgr
    self.MainLoopPause()

  def GameOver(self):
    """\
GameWorld.GameOver()
    Process when game-over.
    Remove game-loop task."""
    self.MainLoopDestroy()

  def GameReConfigure(self):
    """\
GameWorld.GameReConfigure()
    Process when game-reConfigure.
    Reset event name of controller."""
    # in this example, GameReConfigure only re-configure key mapping
    self.ignoreAll()
    self.ControllerEventSet()
    # reset key mapping hint text
    self.upkeyText.setText("[%s]: Accelerate"%(ControllerEventDefine.event_CONTROLLER_UP))
    self.leftkeyText.setText("[%s]: Turn Left (CCW)"%(ControllerEventDefine.event_CONTROLLER_LEFT))
    self.rightkeyText.setText("[%s]: Turn Right (CW)"%(ControllerEventDefine.event_CONTROLLER_RIGHT))
    self.spacekeyText.setText("[%s]: Fire"%(ControllerEventDefine.event_CONTROLLER_BTN0))

  # event handle for controller  ---------------------------------
  def setKey(self, key, val): self.keys[key] = val

  # main loop ----------------------------------------------------
  def gameLoop(self, task):
    dt = task.time - task.last
    task.last = task.time

    self.updateRecord(dt)

    #update ship position
    self.updateShip(dt)

    #check to see if the ship can fire
    if self.keys["fire"] and task.time > task.nextBullet:
      self.fire(task.time)  #If so, call the fire function
      #And disable firing for a bit
      task.nextBullet = task.time + BULLET_REPEAT
    self.keys["fire"] = 0   #Remove the fire flag until the next spacebar press

    #update asteroids
    for obj in self.asteroids: self.updatePos(obj, dt)

    #update bullets
    newBulletArray = []
    for obj in self.bullets:
      self.updatePos(obj, dt)         #Update the bullet
      if self.getExpires(obj) > task.time: newBulletArray.append(obj)
      else: obj.remove()              #Otherwise remove it from the scene
    #Set the bullet array to be the newly updated array
    self.bullets = newBulletArray

    for bullet in self.bullets:
      for i in range(len(self.asteroids)-1, -1, -1):
        if ((bullet.getPos() - self.asteroids[i].getPos()).lengthSquared() <
            (((bullet.getScale().getX() + self.asteroids[i].getScale().getX())
              * .5 ) ** 2)):
          self.setExpires(bullet, 0)       #Schedule the bullet for removal
          self.asteroidHit(i)      #Handle the hit

    if self.TestMode:
      # power overwhiming
      pass
    else:
      # normal mode
      for ast in self.asteroids:
        if ((self.ship.getPos() - ast.getPos()).lengthSquared() <
          (((self.ship.getScale().getX() + ast.getScale().getX()) * .5) ** 2)):

          # ship hit asteroid -> game over, fail
          GameOverInfo.Info["pass"] = False
          GameOverInfo.Info["escaped time"] = self.GameWorldTime
          GameOverEventTrigger()
          return Task.done


    if len(self.asteroids) == 0:
        # all asteroid hit -> game over, pass
        GameOverInfo.Info["pass"] = True
        GameOverInfo.Info["escaped time"] = float(self.GameWorldTime)
        GameOverEventTrigger()
        return Task.done

    return Task.cont

  def asteroidHit(self, index):
    #If the asteroid is small it is simply removed
    if self.asteroids[index].getScale().getX() <= AST_MIN_SCALE:
      self.asteroids[index].remove()
      self.asteroids = self.asteroids[:index]+self.asteroids[index+1:]
    else:
      newScale = self.asteroids[index].getScale().getX() * AST_SIZE_SCALE
      self.asteroids[index].setScale(newScale)  #Rescale it

      vel = self.getVelocity(self.asteroids[index])
      speed = vel.length() * AST_VEL_SCALE
      vel.normalize()
      vel = Vec3(0,1,0).cross(vel)
      vel *= speed
      self.setVelocity(self.asteroids[index], vel)

      newAst = loadObject(scale = newScale)
      self.setVelocity(newAst, vel * -1)
      newAst.setPos(self.asteroids[index].getPos())
      newAst.setTexture(self.asteroids[index].getTexture(), 1)
      self.asteroids.append(newAst)

  def updateShip(self, dt):
    heading = self.ship.getR() #Heading is the roll value for this model
    if self.keys["turnRight"]:
      heading += dt * TURN_RATE
      self.ship.setR(heading %360)
    elif self.keys["turnLeft"]:
      heading -= dt * TURN_RATE
      self.ship.setR(heading%360)

    if self.keys["accel"]:
      heading_rad = DEG_TO_RAD * heading
      newVel = (
        Vec3(sin(heading_rad), 0, cos(heading_rad)) * ACCELERATION * dt)
      newVel += self.getVelocity(self.ship)
      if newVel.lengthSquared() > MAX_VEL_SQ:
        newVel.normalize()
        newVel *= MAX_VEL
      self.setVelocity(self.ship, newVel)

    self.updatePos(self.ship, dt)

  def fire(self, time):
    direction = DEG_TO_RAD * self.ship.getR()
    pos = self.ship.getPos()
    bullet = loadObject("bullet", scale = .2)  #Create the object
    bullet.setPos(pos)
    #Velocity is in relation to the ship
    vel = (self.getVelocity(self.ship) +
           (Vec3(sin(direction), 0, cos(direction)) *
            BULLET_SPEED))
    self.setVelocity(bullet, vel)
    #Set the bullet expiration time to be a certain amount past the current time
    self.setExpires(bullet, time + BULLET_LIFE)

    #Finally, add the new bullet to the list
    self.bullets.append(bullet)

  def updatePos(self, obj, dt):
    vel = self.getVelocity(obj)
    newPos = obj.getPos() + (vel*dt)

    #Check if the object is out of bounds. If so, wrap it
    radius = .5 * obj.getScale().getX()
    if newPos.getX() - radius > SCREEN_X: newPos.setX(-SCREEN_X)
    elif newPos.getX() + radius < -SCREEN_X: newPos.setX(SCREEN_X)
    if newPos.getZ() - radius > SCREEN_Y: newPos.setZ(-SCREEN_Y)
    elif newPos.getZ() + radius < -SCREEN_Y: newPos.setZ(SCREEN_Y)

    obj.setPos(newPos)

  def updateRecord(self, dt):
      preTimeString = "%.1f"%(self.GameWorldTime)
      self.GameWorldTime += dt
      currentTimeString = "%.1f"%(self.GameWorldTime)
      if currentTimeString != preTimeString:
          self.recordText.setText("Escaped: %s s"%(currentTimeString))

  # subroutine of create environment -------------------------------
  def spawnAsteroids(self):
    #self.alive = True               #Control variable for if the ship is alive
    self.asteroids = []             #List that will contain our asteroids
    for i in range(10):
      self.asteroids.append(loadObject("asteroid" + str(randint(1,3)),
                                       scale = AST_INIT_SCALE))

      self.asteroids[i].setX(choice(range(-SCREEN_X, -5) +
                                    range(5, SCREEN_X)))
      #Same thing for Y, but from -15 to 15
      self.asteroids[i].setZ(choice(range(-SCREEN_Y, -5) +
                                    range(5, SCREEN_Y)))
      heading = random()*2*pi  #Heading is a random angle in radians
      v = Vec3(sin(heading), 0, cos(heading)) * AST_INIT_VEL
      self.setVelocity(self.asteroids[i], v)


  # setTag to NodePath / getTag from NodePath --------------------
  def setVelocity(self, obj, val):
    list = [val[0], val[1], val[2]]
    obj.setTag("velocity", cPickle.dumps(list))

  def getVelocity(self, obj):
    list = cPickle.loads(obj.getTag("velocity"))
    return Vec3(list[0], list[1], list[2])

  def setExpires(self, obj, val):
    obj.setTag("expires", str(val))

  def getExpires(self, obj):
    return float(obj.getTag("expires"))


  # internal control functions -----------------------------------
  def GameLoadInfoSetting(self):
      self.TestMode = GameLoadInfo.Info["Test Mode"]

  # controller event  --------------------------------------------
  def ControllerEventSet(self):
    ControllerConfigure()
    # setup event
    self.accept("escape", messenger.send, [event_GAME_GAMEPAUSE] )  # pause game
    #Other keys events set the appropriate value in our key dictionary
    self.accept(ControllerEventDefine.event_CONTROLLER_UP,       self.setKey, ["accel", 1]) # up
    self.accept("%s-up"%(ControllerEventDefine.event_CONTROLLER_UP),    self.setKey, ["accel", 0])
    self.accept(ControllerEventDefine.event_CONTROLLER_LEFT, self.setKey, ["turnLeft", 1]) # left
    self.accept("%s-up"%(ControllerEventDefine.event_CONTROLLER_LEFT), self.setKey, ["turnLeft", 0])
    self.accept(ControllerEventDefine.event_CONTROLLER_RIGHT, self.setKey, ["turnRight", 1]) # right
    self.accept("%s-up"%(ControllerEventDefine.event_CONTROLLER_RIGHT), self.setKey, ["turnRight", 0])
    self.accept(ControllerEventDefine.event_CONTROLLER_BTN0,          self.setKey, ["fire", 1])  # fire

    self.keys = {"turnLeft" : 0, "turnRight": 0,
                 "accel": 0, "fire": 0}

  def ControllerEventDestroy(self):
    self.ignoreAll()

  # 2d gui ----------------------------------------------------------
  def GUISet(self):
    MainFrameGen()
    #This code puts the standard title and instruction text on screen
    self.title = OnscreenText(parent = MainFrame,
                              text="Panda3D: Tutorial - Tasks",
                              style=1, fg=(1,1,0,1),
                              pos=(0.8,-0.95), scale = .07, font = font)
    self.recordText = OnscreenText(parent = MainFrame,
                              text="Escaped:",
                              style=1, fg=(1,1,0,1),
                              pos=(0.8,0.9), scale = .07, font = font)
    self.escapeText =   genLabelText("ESC: Pause", 0)
    self.upkeyText =    genLabelText("[%s]: Accelerate"%(ControllerEventDefine.event_CONTROLLER_UP), 1)
    self.leftkeyText =  genLabelText("[%s]: Turn Left (CCW)"%(ControllerEventDefine.event_CONTROLLER_LEFT), 2)
    self.rightkeyText = genLabelText("[%s]: Turn Right (CW)"%(ControllerEventDefine.event_CONTROLLER_RIGHT), 3)
    self.spacekeyText = genLabelText("[%s]: Fire"%(ControllerEventDefine.event_CONTROLLER_BTN0), 4)

  def GUIDestroy(self):
    MainFrameDestroy()

  # 3d object -------------------------------------------------------
  def ObjectGen(self):
    MainNodeGen()

    self.bg = loadObject("stars", scale = 146, depth = 200,
                         transparency = False) #Load the background starfield

    self.ship = loadObject("ship")             #Load the ship
    self.setVelocity(self.ship, Vec3(0,0,0))    #Initial velocity

    self.bullets = []       #This empty list will contain fired bullets
    self.spawnAsteroids()   #Complete initialization by spawning the asteroids

  def ObjectDestroy(self):
    MainNodeDestroy()
    self.bullets = []
    self.asteroids = []

  # main loop -------------------------------------------------------
  def MainLoopSet(self):
    pass

  def MainLoopRun(self):
    self.gameTask = taskMgr.add(self.gameLoop, task_MAINGAMELOOP["name"], task_MAINGAMELOOP["sort"]) # 0 if not define sort
    self.gameTask.IsRunning = False  # for pausing main loop
    self.gameTask.last = 0         #Task time of the last frame
    self.gameTask.nextBullet = 0   #Task time when the next bullet may be fired

  def MainLoopPause(self):
    taskMgr.remove(task_MAINGAMELOOP["name"])

  def MainLoopDestroy(self):
    taskMgr.remove(task_MAINGAMELOOP["name"])

  # record -------------------------------------------------------
  def GameRecordSet(self):
    self.GameWorldTime = 0

  def GameRecordDestroy(self):
    pass
